﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;

using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Security;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.Schema.ScrumTableV1;

#endregion

namespace ScrumTable.DL.Data.Common.Serialization
{


    /// <summary>
    /// Represents the serializer for data serialization or deserialization.
    /// </summary>
    public class DataStorageSerializer
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const string ResolveFilePattern = "{0}-{1}";
        private readonly ScrumTableV1Schema _schema;
        
        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataContext instance. This constructor is used in
        /// the current library only.
        /// </summary>
        /// <param name="schema">Specifies the schemata of the data to serialize.</param>
        public DataStorageSerializer(ScrumTableV1Schema schema)
        {
            PreCondition.AssertNotNull(schema, "schema");

            _schema = schema;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Serializes the given data storage. The given file system path
        /// is used in order to store the data on the file system in xml format.
        /// </summary>
        /// <param name="fileSystemPath">Path of the file system to store.</param>
        /// <param name="toSerialize">Object data storage to serialize.</param>
        public void Serialize(string fileSystemPath, IDataStorage toSerialize)
        {
            Serialize(fileSystemPath, toSerialize, new DataResolver(ConnectionInformation.Empty));
        }

        /// <summary>
        /// Serializes the given data storage. The given file system path
        /// is used in order to store the data on the file system in xml format.
        /// </summary>
        /// <param name="fileSystemPath">Path of the file system to store.</param>
        /// <param name="toSerialize">Object data storage to serialize.</param>
        /// <param name="resolver">Resolves data from external storages to a local storage.</param>
        public void Serialize(string fileSystemPath, IDataStorage toSerialize, IDataResolver resolver)
        {
            PreCondition.AssertNotNullOrEmpty(fileSystemPath, "fileSystemPath");
            PreCondition.AssertNotNull(toSerialize, "toSerialize");
            PreCondition.AssertNotNull(toSerialize, "resolver");
            
            FileInfo file = new FileInfo(fileSystemPath);

            if (file.Exists)
                file.Delete();

            SerializationContext context = new SerializationContext
                                               {
                                                   OutputDirectory = file.Directory.FullName,
                                                   OutputFile = file.FullName,
                                                   Resolver = resolver
                                               };

            DataStorage toStore = InitializeSerializationTree(toSerialize, context);

            using (FileStream stream = file.OpenWrite())
            {
                new XmlSerializer(typeof(DataStorage)).Serialize(stream, toStore);
            }
        }

        /// <summary>
        /// Deseriaizes the given file system object and stores the deserialized
        /// values into the given data storage.
        /// </summary>
        /// <param name="fileSystemPath">Path of the file system to retreive.</param>
        /// <param name="toFill">Object data storage to store the deserialized values.</param>
        public void Deserialize(string fileSystemPath, IDataStorage toFill)
        {
            PreCondition.AssertNotNullOrEmpty(fileSystemPath, "fileSystemPath");
            PreCondition.AssertNotNull(toFill, "toFill");

            toFill.Clear();
            DataObjectIndex<IDataObject> rawIndices = new DataObjectIndex<IDataObject>(Deserialize(fileSystemPath));
            FillDataStorageTree(toFill, rawIndices);
            UpdateReferences(toFill, rawIndices);
        }

        /// <summary>
        /// Deserializes the given file system object and returns the deserialized
        /// values.
        /// </summary>
        /// <param name="fileSystemPath">Path of the file system to retreive.</param>
        public DataStorage Deserialize(string fileSystemPath)
        {
            PreCondition.AssertNotNullOrEmpty(fileSystemPath, "fileSystemPath");

            DataStorage data;

            // load data
            using (FileStream stream = File.Open(fileSystemPath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                data = (DataStorage)(new XmlSerializer(typeof(DataStorage))).Deserialize(stream);
            }
            return data;
        }

        private DataStorage InitializeSerializationTree(IDataStorage toSerialize, SerializationContext context)
        {
            DataStorage storage = new DataStorage();

            foreach (SchemaBase schema in _schema.Schemata)
            {
                storage.DataAccessObjects.Add(
                    InitializeDataAccessObjectTree(
                        toSerialize[schema.SchemaName],
                        schema,
                        context));
            }
            return storage;
        }

        private DataAccessObject InitializeDataAccessObjectTree(IDataAccessObject toSerialize, SchemaBase schema, SerializationContext context)
        {
            DataAccessObject dataAccesObject = new DataAccessObject();
            dataAccesObject.Schema = schema.SchemaName;

            if (!schema.IsSchemaElementReadOnly)
            {
                foreach (KeyValuePair<ValueChain<string>, ICollection<IDataObject>> keyObjectsPair in toSerialize.GetAll())
                {
                    var dataObjects = from dataObject in keyObjectsPair.Value
                                      orderby dataObject.OrderIndex
                                      select dataObject;

                    foreach (IDataObject dataObject in dataObjects)
                    {
                        dataAccesObject.DataObjects.Add(InitializeDataObjectTree(dataObject, keyObjectsPair.Key, schema, context));
                    }
                }
            }
            return dataAccesObject;
        }

        private DataObject InitializeDataObjectTree(IDataObject toSerialize, ValueChain<string> parentChain, SchemaBase schema, SerializationContext context)
        {
            DataObject dataObject = new DataObject();
            dataObject.Id = toSerialize.Id;
            dataObject.OrderIndex = toSerialize.OrderIndex;
            dataObject.ParentIds = parentChain.Chain;

            foreach (SchemaBase schemaElement in schema.Schemata)
            {
                dataObject.DataValues.Add(InitializeDataValueTree(toSerialize, schemaElement, context));
            }
            return dataObject;
        }

        private DataValue InitializeDataValueTree(IDataObject toSerialize, SchemaBase schema, SerializationContext context)
        {
            DataValue value = new DataValue();
            value.Name = schema.SchemaName;
            value.SetData(
                toSerialize[schema.SchemaName],
                Path.Combine(context.OutputDirectory, string.Format(ResolveFilePattern, toSerialize.Id, schema.SchemaName)),
                context.Resolver);
            return value;
        }


        private void FillDataStorageTree(IDataStorage toFill, DataObjectIndex<IDataObject> rawIndices)
        {
            foreach (DataObjectIndexEntry<IDataObject> dataObjectIndex in rawIndices)
            {
                FillDataObject(toFill, rawIndices, dataObjectIndex);
            }
        }

        private void FillDataObject(IDataStorage toFill, DataObjectIndex<IDataObject> rawIndices, DataObjectIndexEntry<IDataObject> rawDataIndexEntry)
        {
            if (rawDataIndexEntry.DataLayerObject != null) // value has been stored already
                return;

            if (rawDataIndexEntry.RawParentIds == ValueChain<string>.Empty) // top level objects
            {
                FillDataObjectIndexIntoTree(
                    toFill,
                    ValueChain<string>.Empty,
                    rawDataIndexEntry);
            }
            else // child level objects, recursive calls for incomplete parents
            {
                if (rawIndices[rawDataIndexEntry.RawParentIds].DataLayerObject == null)
                {
                    // parent element does not exist yet, force creation in a recursive function call
                    FillDataObject(toFill, rawIndices, rawIndices[rawDataIndexEntry.RawParentIds]);
                }

                // okay, parent exist, let's create the current element
                FillDataObjectIndexIntoTree(
                    toFill,
                    rawIndices[rawDataIndexEntry.RawParentIds].DataLayerObjectIds,
                    rawDataIndexEntry);
            }
        }

        private void FillDataObjectIndexIntoTree(IDataStorage toFill, ValueChain<string> parentValue, DataObjectIndexEntry<IDataObject> rawDataIndexEntry)
        {
            rawDataIndexEntry.DataLayerObject = toFill[rawDataIndexEntry.RawParent.Schema].Insert(parentValue);
            rawDataIndexEntry.DataLayerObjectIds = new ValueChain<string>(parentValue, rawDataIndexEntry.DataLayerObject.Id);

            FillDataObjectTree(
                rawDataIndexEntry.DataLayerObject, rawDataIndexEntry.RawDataObject,
                _schema[rawDataIndexEntry.RawParent.Schema]);
        }

        private void FillDataObjectTree(IDataObject toFill, DataObject toDeserialize, SchemaBase schema)
        {
            foreach (SchemaBase valueSchema in schema.Schemata)
            {
                DataValue value = toDeserialize[valueSchema.SchemaName];

                if (value != null)
                {
                    toFill[valueSchema.SchemaName] = value.GetData(valueSchema.SchemaType);
                }
            }
        }

        private void UpdateReferences(IDataStorage toUpdate, DataObjectIndex<IDataObject> rawIndices)
        {
            foreach (SchemaBase dataAccessObjectSchema in _schema.Schemata)
            {
                UpdateReferencesOnDataAccessObject(toUpdate[dataAccessObjectSchema.SchemaName], rawIndices, dataAccessObjectSchema);
            }
        }

        private void UpdateReferencesOnDataAccessObject(IDataAccessObject toUpdate, DataObjectIndex<IDataObject> rawIndices, SchemaBase dataAccessObjectSchema)
        {
            foreach (KeyValuePair<ValueChain<string>, ICollection<IDataObject>> keyObjectsPair in toUpdate.GetAll())
            {
                foreach (IDataObject dataObject in keyObjectsPair.Value)
                {
                    UpdateReferencesOnDataObject(dataObject, rawIndices, dataAccessObjectSchema);
                }
            }
        }

        private void UpdateReferencesOnDataObject(IDataObject toUpdate, DataObjectIndex<IDataObject> rawIndices, SchemaBase dataAccessObjectSchema)
        {
            foreach (SchemaBase schemaElement in dataAccessObjectSchema.Schemata)
            {
                if (schemaElement.IsSchemaReference && toUpdate[schemaElement] != null)
                    toUpdate[schemaElement] = rawIndices[(ValueChain<string>)toUpdate[schemaElement]].DataLayerObjectIds;
            }
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion



        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        /// <summary>
        /// Contains information about the current serialization state.
        /// </summary>
        private class SerializationContext
        {
            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            internal string OutputFile { get; set; }
            internal string OutputDirectory { get; set; }
            internal IDataResolver Resolver { get; set; }

            #endregion
        }
    }
}